package zyz.framework.dao.interceptor;

import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import zyz.framework.entity.Page;
import zyz.framework.util.ReflectUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 分页拦截器，用于拦截需要进行分页查询的操作，然后对其进行分页处理。
 * 利用拦截器实现Mybatis分页的原理：
 * 要利用JDBC对数据库进行操作就必须要有一个对应的Statement对象，Mybatis在执行Sql语句前就会产生一个包含Sql语句的Statement对象，而且对应的Sql语句
 * 是在Statement之前产生的，所以我们就可以在它生成Statement之前对用来生成Statement的Sql语句下手。在Mybatis中Statement语句是通过RoutingStatementHandler对象的
 * prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法，然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句，之后再调用
 * StatementHandler对象的prepare方法，即调用invocation.proceed()。
 * 对于分页而言，在拦截器里面我们还需要做的一个操作就是统计满足当前条件的记录一共有多少，这是通过获取到了原始的Sql语句后，把它改为对应的统计语句再利用Mybatis封装好的参数和设
 * 置参数的功能把Sql语句中的参数进行替换，之后再执行查询记录数的Sql语句进行总记录数的统计。
 */
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class,Integer.class})})
@Component
public class PageInterceptor implements Interceptor {

    private Logger logger = Logger.getLogger(this.getClass());

    /*
     * 数据库类型，不同的数据库有不同的分页方法
     */
    private String databaseType;

    public void setDatabaseType(String databaseType) {
        this.databaseType = databaseType;
    }

    /**
     * 拦截后要执行的方法
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        if (!(invocation.getTarget() instanceof RoutingStatementHandler)) return invocation.proceed();

        RoutingStatementHandler statementHandler = (RoutingStatementHandler) invocation.getTarget();
        StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(statementHandler, "delegate");

        BoundSql boundSql = delegate.getBoundSql();
        Object parameterObj = boundSql.getParameterObject();
        if (parameterObj instanceof Page<?>) {
            Page<?> page = (Page<?>) parameterObj;

            String sql = boundSql.getSql();
            Connection connection = (Connection) invocation.getArgs()[0];
            MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");

            this.setTotalRecord(page, mappedStatement, connection);

            String pageSql = this.getPageSql(page, sql);
            ReflectUtil.setFieldValue(boundSql, "sql", pageSql);
        }
        Object paramObj = delegate.getParameterHandler().getParameterObject();
        if (paramObj == null || !(paramObj instanceof Map)) return invocation.proceed();
        Map<String, Object> paramMap = (Map<String, Object>) paramObj;
        if (!paramMap.containsKey("getClazz")) return invocation.proceed();

        Class<?> resultEntity = null;
        try {
            resultEntity = (Class<?>) paramMap.get("getClazz");
        } catch (Exception e) {
        }

        MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
        mappedStatement.getConfiguration().setMapUnderscoreToCamelCase(true);

        ResultMap.Builder builder = new ResultMap.Builder(mappedStatement.getConfiguration(), mappedStatement.getResultMaps().get(0).getId(), resultEntity, new ArrayList<ResultMapping>());
        ReflectUtil.setFieldValue(mappedStatement, "resultMaps", Collections.singletonList(builder.build()));

        return invocation.proceed();
    }

    /**
     * 拦截器对应的封装原始对象的方法
     */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    /**
     * 设置注册拦截器时设定的属性
     */
    @Override
    public void setProperties(Properties properties) {
        this.databaseType = properties.getProperty("databaseType");
    }


    /**
     * 根据page对象获取对应的分页查询Sql语句，这里只做了两种数据库类型，Mysql和Oracle
     * 其它的数据库都 没有进行分页
     *
     * @param page 分页对象
     * @param sql  原sql语句
     * @return
     */
    public String getPageSql(Page<?> page, String sql) {
        StringBuffer sqlBuffer = new StringBuffer(sql);

        if ("mysql".equalsIgnoreCase(databaseType)) {
            return getMysqlPageSql(page, sqlBuffer);
        } else if ("oracle".equalsIgnoreCase(databaseType)) {
            return getOraclePageSql(page, sqlBuffer);
        }

        return sqlBuffer.toString();
    }

    /**
     * 获取Mysql数据库的分页查询语句
     *
     * @param page      分页对象
     * @param sqlBuffer 包含原sql语句的StringBuffer对象
     * @return Mysql数据库分页语句
     */
    private String getMysqlPageSql(Page<?> page, StringBuffer sqlBuffer) {
        int offset = (page.getPage() - 1) * page.getEpage();
        sqlBuffer.append(" limit ").append(offset).append(",").append(page.getEpage());
        return sqlBuffer.toString();
    }

    /**
     * 获取Oracle数据库的分页查询语句
     *
     * @param page      分页对象
     * @param sqlBuffer 包含原sql语句的StringBuffer对象
     * @return Oracle数据库的分页查询语句
     */
    private String getOraclePageSql(Page<?> page, StringBuffer sqlBuffer) {
        int offset = (page.getPage() - 1) * page.getEpage() + 1;
        sqlBuffer.insert(0, "select u.*, rownum r from (");
        sqlBuffer.append(") u where rownum < ").append(offset + page.getEpage());
        sqlBuffer.insert(0, "select * from (");
        sqlBuffer.append(") where r >= ").append(offset);
        return sqlBuffer.toString();
    }

    /**
     * 给当前的参数对象page设置总记录数
     *
     * @param page            Mapper映射语句对应的参数对象
     * @param mappedStatement Mapper映射语句
     * @param connection      当前的数据库连接
     */
    private void setTotalRecord(Page<?> page, MappedStatement mappedStatement, Connection connection) {
        BoundSql boundSql = mappedStatement.getBoundSql(page);
        String sql = boundSql.getSql();

        String countSql = this.getCountSql(sql);
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page);
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql);
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = connection.prepareStatement(countSql);
            parameterHandler.setParameters(pstmt);
            rs = pstmt.executeQuery();
            if (rs.next()) {
                int totalRecord = rs.getInt(1);
                page.setTotal_items(totalRecord);
            }
        } catch (SQLException e) {
            logger.error(e.getMessage(), e);
        }
    }

    /**
     * 根据原Sql语句获取对应的查询总记录数的Sql语句
     *
     * @param sql
     * @return
     */
    private String getCountSql(String sql) {
        int subQueryIndex = sql.indexOf("(");
        int fromIndex = sql.toLowerCase().indexOf("from");
        if (fromIndex > subQueryIndex) {
            String tmpSql = sql;
            Pattern pattern = Pattern.compile("\\([^(^)]*\\)");
            Matcher matcher = pattern.matcher(tmpSql);
            while (matcher.find()) {
                tmpSql = matcher.replaceFirst("");

                subQueryIndex = tmpSql.indexOf("(");
                fromIndex = tmpSql.toLowerCase().indexOf("from");
                if (subQueryIndex <= 0 || fromIndex < subQueryIndex) {
                    sql = tmpSql;
                    break;
                }
                matcher = pattern.matcher(tmpSql);
            }
        }

        return "select count(1) " + sql.substring(fromIndex);
    }
}
